{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# For tips on running notebooks in Google Colab, see\n",
    "# https://pytorch.org/tutorials/beginner/colab\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pendulum: Writing your environment and transforms with TorchRL\n",
    "==============================================================\n",
    "\n",
    "**Author**: [Vincent Moens](https://github.com/vmoens)\n",
    "\n",
    "Creating an environment (a simulator or an interface to a physical\n",
    "control system) is an integrative part of reinforcement learning and\n",
    "control engineering.\n",
    "\n",
    "TorchRL provides a set of tools to do this in multiple contexts. This\n",
    "tutorial demonstrates how to use PyTorch and TorchRL code a pendulum\n",
    "simulator from the ground up. It is freely inspired by the Pendulum-v1\n",
    "implementation from [OpenAI-Gym/Farama-Gymnasium control\n",
    "library](https://github.com/Farama-Foundation/Gymnasium).\n",
    "\n",
    "![Simple\n",
    "Pendulum](https://pytorch.org/tutorials/_static/img/pendulum.gif){.align-center}\n",
    "\n",
    "Key learnings:\n",
    "\n",
    "-   How to design an environment in TorchRL:\n",
    "\n",
    "    -   Writing specs (input, observation and reward);\n",
    "    -   Implementing behavior: seeding, reset and step.\n",
    "\n",
    "-   Transforming your environment inputs and outputs, and writing your\n",
    "    own transforms;\n",
    "\n",
    "-   How to use `~tensordict.TensorDict`{.interpreted-text role=\"class\"}\n",
    "    to carry arbitrary data structures through the `codebase`.\n",
    "\n",
    "    In the process, we will touch three crucial components of TorchRL:\n",
    "\n",
    "-   [environments](https://pytorch.org/rl/reference/envs.html)\n",
    "\n",
    "-   [transforms](https://pytorch.org/rl/reference/envs.html#transforms)\n",
    "\n",
    "-   [models (policy and value\n",
    "    function)](https://pytorch.org/rl/reference/modules.html)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To give a sense of what can be achieved with TorchRL\\'s environments, we\n",
    "will be designing a *stateless* environment. While stateful environments\n",
    "keep track of the latest physical state encountered and rely on this to\n",
    "simulate the state-to-state transition, stateless environments expect\n",
    "the current state to be provided to them at each step, along with the\n",
    "action undertaken. TorchRL supports both types of environments, but\n",
    "stateless environments are more generic and hence cover a broader range\n",
    "of features of the environment API in TorchRL.\n",
    "\n",
    "Modeling stateless environments gives users full control over the input\n",
    "and outputs of the simulator: one can reset an experiment at any stage\n",
    "or actively modify the dynamics from the outside. However, it assumes\n",
    "that we have some control over a task, which may not always be the case:\n",
    "solving a problem where we cannot control the current state is more\n",
    "challenging but has a much wider set of applications.\n",
    "\n",
    "Another advantage of stateless environments is that they can enable\n",
    "batched execution of transition simulations. If the backend and the\n",
    "implementation allow it, an algebraic operation can be executed\n",
    "seamlessly on scalars, vectors, or tensors. This tutorial gives such\n",
    "examples.\n",
    "\n",
    "This tutorial will be structured as follows:\n",
    "\n",
    "-   We will first get acquainted with the environment properties: its\n",
    "    shape (`batch_size`), its methods (mainly\n",
    "    `~torchrl.envs.EnvBase.step`{.interpreted-text role=\"meth\"},\n",
    "    `~torchrl.envs.EnvBase.reset`{.interpreted-text role=\"meth\"} and\n",
    "    `~torchrl.envs.EnvBase.set_seed`{.interpreted-text role=\"meth\"}) and\n",
    "    finally its specs.\n",
    "-   After having coded our simulator, we will demonstrate how it can be\n",
    "    used during training with transforms.\n",
    "-   We will explore new avenues that follow from the TorchRL\\'s API,\n",
    "    including: the possibility of transforming inputs, the vectorized\n",
    "    execution of the simulation and the possibility of backpropagation\n",
    "    through the simulation graph.\n",
    "-   Finally, we will train a simple policy to solve the system we\n",
    "    implemented.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/rocketsky/miniconda3/envs/mac-rl/lib/python3.10/site-packages/torchrl/data/replay_buffers/samplers.py:37: UserWarning: Failed to import torchrl C++ binaries. Some modules (eg, prioritized replay buffers) may not work with your installation. If you installed TorchRL from PyPI, please report the bug on TorchRL github. If you installed TorchRL locally and/or in development mode, check that you have all the required compiling packages.\n",
      "  warnings.warn(EXTENSION_WARNING)\n"
     ]
    }
   ],
   "source": [
    "from collections import defaultdict\n",
    "from typing import Optional\n",
    "\n",
    "import numpy as np\n",
    "import torch\n",
    "import tqdm\n",
    "from tensordict import TensorDict, TensorDictBase\n",
    "from tensordict.nn import TensorDictModule\n",
    "from torch import nn\n",
    "\n",
    "from torchrl.data import BoundedTensorSpec, CompositeSpec, UnboundedContinuousTensorSpec\n",
    "from torchrl.envs import (\n",
    "    CatTensors,\n",
    "    EnvBase,\n",
    "    Transform,\n",
    "    TransformedEnv,\n",
    "    UnsqueezeTransform,\n",
    ")\n",
    "from torchrl.envs.transforms.transforms import _apply_to_composite\n",
    "from torchrl.envs.utils import check_env_specs, step_mdp\n",
    "\n",
    "DEFAULT_X = np.pi\n",
    "DEFAULT_Y = 1.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are four things you must take care of when designing a new\n",
    "environment class:\n",
    "\n",
    "-   `EnvBase._reset`{.interpreted-text role=\"meth\"}, which codes for the\n",
    "    resetting of the simulator at a (potentially random) initial state;\n",
    "-   `EnvBase._step`{.interpreted-text role=\"meth\"} which codes for the\n",
    "    state transition dynamic;\n",
    "-   `EnvBase._set_seed`{.interpreted-text role=\"meth\"}\\` which\n",
    "    implements the seeding mechanism;\n",
    "-   the environment specs.\n",
    "\n",
    "Let us first describe the problem at hand: we would like to model a\n",
    "simple pendulum over which we can control the torque applied on its\n",
    "fixed point. Our goal is to place the pendulum in upward position\n",
    "(angular position at 0 by convention) and having it standing still in\n",
    "that position. To design our dynamic system, we need to define two\n",
    "equations: the motion equation following an action (the torque applied)\n",
    "and the reward equation that will constitute our objective function.\n",
    "\n",
    "For the motion equation, we will update the angular velocity following:\n",
    "\n",
    "$$\\dot{\\theta}_{t+1} = \\dot{\\theta}_t + (3 * g / (2 * L) * \\sin(\\theta_t) + 3 / (m * L^2) * u) * dt$$\n",
    "\n",
    "where $\\dot{\\theta}$ is the angular velocity in rad/sec, $g$ is the\n",
    "gravitational force, $L$ is the pendulum length, $m$ is its mass,\n",
    "$\\theta$ is its angular position and $u$ is the torque. The angular\n",
    "position is then updated according to\n",
    "\n",
    "$$\\theta_{t+1} = \\theta_{t} + \\dot{\\theta}_{t+1} dt$$\n",
    "\n",
    "We define our reward as\n",
    "\n",
    "$$r = -(\\theta^2 + 0.1 * \\dot{\\theta}^2 + 0.001 * u^2)$$\n",
    "\n",
    "which will be maximized when the angle is close to 0 (pendulum in upward\n",
    "position), the angular velocity is close to 0 (no motion) and the torque\n",
    "is 0 too.\n",
    "\n",
    "Coding the effect of an action: `~torchrl.envs.EnvBase._step`{.interpreted-text role=\"func\"}\n",
    "============================================================================================\n",
    "\n",
    "The step method is the first thing to consider, as it will encode the\n",
    "simulation that is of interest to us. In TorchRL, the\n",
    "`~torchrl.envs.EnvBase`{.interpreted-text role=\"class\"} class has a\n",
    "`EnvBase.step`{.interpreted-text role=\"meth\"} method that receives a\n",
    "`tensordict.TensorDict`{.interpreted-text role=\"class\"} instance with an\n",
    "`\"action\"` entry indicating what action is to be taken.\n",
    "\n",
    "To facilitate the reading and writing from that `tensordict` and to make\n",
    "sure that the keys are consistent with what\\'s expected from the\n",
    "library, the simulation part has been delegated to a private abstract\n",
    "method `_step`{.interpreted-text role=\"meth\"} which reads input data\n",
    "from a `tensordict`, and writes a *new* `tensordict` with the output\n",
    "data.\n",
    "\n",
    "The `_step`{.interpreted-text role=\"func\"} method should do the\n",
    "following:\n",
    "\n",
    "> 1.  Read the input keys (such as `\"action\"`) and execute the\n",
    ">     simulation based on these;\n",
    "> 2.  Retrieve observations, done state and reward;\n",
    "> 3.  Write the set of observation values along with the reward and done\n",
    ">     state at the corresponding entries in a new\n",
    ">     `TensorDict`{.interpreted-text role=\"class\"}.\n",
    "\n",
    "Next, the `~torchrl.envs.EnvBase.step`{.interpreted-text role=\"meth\"}\n",
    "method will merge the output of\n",
    "`~torchrl.envs.EnvBase.step`{.interpreted-text role=\"meth\"} in the input\n",
    "`tensordict` to enforce input/output consistency.\n",
    "\n",
    "Typically, for stateful environments, this will look like this:\n",
    "\n",
    "``` {.}\n",
    ">>> policy(env.reset())\n",
    ">>> print(tensordict)\n",
    "TensorDict(\n",
    "    fields={\n",
    "        action: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
    "        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
    "        observation: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
    "    batch_size=torch.Size([]),\n",
    "    device=cpu,\n",
    "    is_shared=False)\n",
    ">>> env.step(tensordict)\n",
    ">>> print(tensordict)\n",
    "TensorDict(\n",
    "    fields={\n",
    "        action: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
    "        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
    "        next: TensorDict(\n",
    "            fields={\n",
    "                done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
    "                observation: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
    "                reward: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
    "            batch_size=torch.Size([]),\n",
    "            device=cpu,\n",
    "            is_shared=False),\n",
    "        observation: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
    "    batch_size=torch.Size([]),\n",
    "    device=cpu,\n",
    "    is_shared=False)\n",
    "```\n",
    "\n",
    "Notice that the root `tensordict` has not changed, the only modification\n",
    "is the appearance of a new `\"next\"` entry that contains the new\n",
    "information.\n",
    "\n",
    "In the Pendulum example, our `_step`{.interpreted-text role=\"meth\"}\n",
    "method will read the relevant entries from the input `tensordict` and\n",
    "compute the position and velocity of the pendulum after the force\n",
    "encoded by the `\"action\"` key has been applied onto it. We compute the\n",
    "new angular position of the pendulum `\"new_th\"` as the result of the\n",
    "previous position `\"th\"` plus the new velocity `\"new_thdot\"` over a time\n",
    "interval `dt`.\n",
    "\n",
    "Since our goal is to turn the pendulum up and maintain it still in that\n",
    "position, our `cost` (negative reward) function is lower for positions\n",
    "close to the target and low speeds. Indeed, we want to discourage\n",
    "positions that are far from being \\\"upward\\\" and/or speeds that are far\n",
    "from 0.\n",
    "\n",
    "In our example, `EnvBase._step`{.interpreted-text role=\"meth\"} is\n",
    "encoded as a static method since our environment is stateless. In\n",
    "stateful settings, the `self` argument is needed as the state needs to\n",
    "be read from the environment.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def _step(tensordict):\n",
    "    th, thdot = tensordict[\"th\"], tensordict[\"thdot\"]  # th := theta\n",
    "\n",
    "    g_force = tensordict[\"params\", \"g\"]\n",
    "    mass = tensordict[\"params\", \"m\"]\n",
    "    length = tensordict[\"params\", \"l\"]\n",
    "    dt = tensordict[\"params\", \"dt\"]\n",
    "    u = tensordict[\"action\"].squeeze(-1)\n",
    "    u = u.clamp(-tensordict[\"params\", \"max_torque\"], tensordict[\"params\", \"max_torque\"])\n",
    "    costs = angle_normalize(th) ** 2 + 0.1 * thdot**2 + 0.001 * (u**2)\n",
    "\n",
    "    new_thdot = (\n",
    "        thdot\n",
    "        + (3 * g_force / (2 * length) * th.sin() + 3.0 / (mass * length**2) * u) * dt\n",
    "    )\n",
    "    new_thdot = new_thdot.clamp(\n",
    "        -tensordict[\"params\", \"max_speed\"], tensordict[\"params\", \"max_speed\"]\n",
    "    )\n",
    "    new_th = th + new_thdot * dt\n",
    "    reward = -costs.view(*tensordict.shape, 1)\n",
    "    done = torch.zeros_like(reward, dtype=torch.bool)\n",
    "    out = TensorDict(\n",
    "        {\n",
    "            \"th\": new_th,\n",
    "            \"thdot\": new_thdot,\n",
    "            \"params\": tensordict[\"params\"],\n",
    "            \"reward\": reward,\n",
    "            \"done\": done,\n",
    "        },\n",
    "        tensordict.shape,\n",
    "    )\n",
    "    return out\n",
    "\n",
    "\n",
    "def angle_normalize(x):\n",
    "    return ((x + torch.pi) % (2 * torch.pi)) - torch.pi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Resetting the simulator: `~torchrl.envs.EnvBase._reset`{.interpreted-text role=\"func\"}\n",
    "======================================================================================\n",
    "\n",
    "The second method we need to care about is the\n",
    "`~torchrl.envs.EnvBase._reset`{.interpreted-text role=\"meth\"} method.\n",
    "Like `~torchrl.envs.EnvBase._step`{.interpreted-text role=\"meth\"}, it\n",
    "should write the observation entries and possibly a done state in the\n",
    "`tensordict` it outputs (if the done state is omitted, it will be filled\n",
    "as `False` by the parent method\n",
    "`~torchrl.envs.EnvBase.reset`{.interpreted-text role=\"meth\"}). In some\n",
    "contexts, it is required that the `_reset` method receives a command\n",
    "from the function that called it (for example, in multi-agent settings\n",
    "we may want to indicate which agents need to be reset). This is why the\n",
    "`~torchrl.envs.EnvBase._reset`{.interpreted-text role=\"meth\"} method\n",
    "also expects a `tensordict` as input, albeit it may perfectly be empty\n",
    "or `None`.\n",
    "\n",
    "The parent `EnvBase.reset`{.interpreted-text role=\"meth\"} does some\n",
    "simple checks like the `EnvBase.step`{.interpreted-text role=\"meth\"}\n",
    "does, such as making sure that a `\"done\"` state is returned in the\n",
    "output `tensordict` and that the shapes match what is expected from the\n",
    "specs.\n",
    "\n",
    "For us, the only important thing to consider is whether\n",
    "`EnvBase._reset`{.interpreted-text role=\"meth\"} contains all the\n",
    "expected observations. Once more, since we are working with a stateless\n",
    "environment, we pass the configuration of the pendulum in a nested\n",
    "`tensordict` named `\"params\"`.\n",
    "\n",
    "In this example, we do not pass a done state as this is not mandatory\n",
    "for `_reset`{.interpreted-text role=\"meth\"} and our environment is\n",
    "non-terminating, so we always expect it to be `False`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def _reset(self, tensordict):\n",
    "    if tensordict is None or tensordict.is_empty():\n",
    "        # if no ``tensordict`` is passed, we generate a single set of hyperparameters\n",
    "        # Otherwise, we assume that the input ``tensordict`` contains all the relevant\n",
    "        # parameters to get started.\n",
    "        tensordict = self.gen_params(batch_size=self.batch_size)\n",
    "\n",
    "    high_th = torch.tensor(DEFAULT_X, device=self.device)\n",
    "    high_thdot = torch.tensor(DEFAULT_Y, device=self.device)\n",
    "    low_th = -high_th\n",
    "    low_thdot = -high_thdot\n",
    "\n",
    "    # for non batch-locked environments, the input ``tensordict`` shape dictates the number\n",
    "    # of simulators run simultaneously. In other contexts, the initial\n",
    "    # random state's shape will depend upon the environment batch-size instead.\n",
    "    th = (\n",
    "        torch.rand(tensordict.shape, generator=self.rng, device=self.device)\n",
    "        * (high_th - low_th)\n",
    "        + low_th\n",
    "    )\n",
    "    thdot = (\n",
    "        torch.rand(tensordict.shape, generator=self.rng, device=self.device)\n",
    "        * (high_thdot - low_thdot)\n",
    "        + low_thdot\n",
    "    )\n",
    "    out = TensorDict(\n",
    "        {\n",
    "            \"th\": th,\n",
    "            \"thdot\": thdot,\n",
    "            \"params\": tensordict[\"params\"],\n",
    "        },\n",
    "        batch_size=tensordict.shape,\n",
    "    )\n",
    "    return out"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Environment metadata: `env.*_spec`\n",
    "==================================\n",
    "\n",
    "The specs define the input and output domain of the environment. It is\n",
    "important that the specs accurately define the tensors that will be\n",
    "received at runtime, as they are often used to carry information about\n",
    "environments in multiprocessing and distributed settings. They can also\n",
    "be used to instantiate lazily defined neural networks and test scripts\n",
    "without actually querying the environment (which can be costly with\n",
    "real-world physical systems for instance).\n",
    "\n",
    "There are four specs that we must code in our environment:\n",
    "\n",
    "-   `EnvBase.observation_spec`{.interpreted-text role=\"obj\"}: This will\n",
    "    be a `~torchrl.data.CompositeSpec`{.interpreted-text role=\"class\"}\n",
    "    instance where each key is an observation (a\n",
    "    `CompositeSpec`{.interpreted-text role=\"class\"} can be viewed as a\n",
    "    dictionary of specs).\n",
    "-   `EnvBase.action_spec`{.interpreted-text role=\"obj\"}: It can be any\n",
    "    type of spec, but it is required that it corresponds to the\n",
    "    `\"action\"` entry in the input `tensordict`;\n",
    "-   `EnvBase.reward_spec`{.interpreted-text role=\"obj\"}: provides\n",
    "    information about the reward space;\n",
    "-   `EnvBase.done_spec`{.interpreted-text role=\"obj\"}: provides\n",
    "    information about the space of the done flag.\n",
    "\n",
    "TorchRL specs are organized in two general containers: `input_spec`\n",
    "which contains the specs of the information that the step function reads\n",
    "(divided between `action_spec` containing the action and `state_spec`\n",
    "containing all the rest), and `output_spec` which encodes the specs that\n",
    "the step outputs (`observation_spec`, `reward_spec` and `done_spec`). In\n",
    "general, you should not interact directly with `output_spec` and\n",
    "`input_spec` but only with their content: `observation_spec`,\n",
    "`reward_spec`, `done_spec`, `action_spec` and `state_spec`. The reason\n",
    "if that the specs are organized in a non-trivial way within\n",
    "`output_spec` and `input_spec` and neither of these should be directly\n",
    "modified.\n",
    "\n",
    "In other words, the `observation_spec` and related properties are\n",
    "convenient shortcuts to the content of the output and input spec\n",
    "containers.\n",
    "\n",
    "TorchRL offers multiple `~torchrl.data.TensorSpec`{.interpreted-text\n",
    "role=\"class\"}\n",
    "[subclasses](https://pytorch.org/rl/reference/data.html#tensorspec) to\n",
    "encode the environment\\'s input and output characteristics.\n",
    "\n",
    "Specs shape\n",
    "-----------\n",
    "\n",
    "The environment specs leading dimensions must match the environment\n",
    "batch-size. This is done to enforce that every component of an\n",
    "environment (including its transforms) have an accurate representation\n",
    "of the expected input and output shapes. This is something that should\n",
    "be accurately coded in stateful settings.\n",
    "\n",
    "For non batch-locked environments, such as the one in our example (see\n",
    "below), this is irrelevant as the environment batch size will most\n",
    "likely be empty.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def _make_spec(self, td_params):\n",
    "    # Under the hood, this will populate self.output_spec[\"observation\"]\n",
    "    self.observation_spec = CompositeSpec(\n",
    "        th=BoundedTensorSpec(\n",
    "            low=-torch.pi,\n",
    "            high=torch.pi,\n",
    "            shape=(),\n",
    "            dtype=torch.float32,\n",
    "        ),\n",
    "        thdot=BoundedTensorSpec(\n",
    "            low=-td_params[\"params\", \"max_speed\"],\n",
    "            high=td_params[\"params\", \"max_speed\"],\n",
    "            shape=(),\n",
    "            dtype=torch.float32,\n",
    "        ),\n",
    "        # we need to add the ``params`` to the observation specs, as we want\n",
    "        # to pass it at each step during a rollout\n",
    "        params=make_composite_from_td(td_params[\"params\"]),\n",
    "        shape=(),\n",
    "    )\n",
    "    # since the environment is stateless, we expect the previous output as input.\n",
    "    # For this, ``EnvBase`` expects some state_spec to be available\n",
    "    self.state_spec = self.observation_spec.clone()\n",
    "    # action-spec will be automatically wrapped in input_spec when\n",
    "    # `self.action_spec = spec` will be called supported\n",
    "    self.action_spec = BoundedTensorSpec(\n",
    "        low=-td_params[\"params\", \"max_torque\"],\n",
    "        high=td_params[\"params\", \"max_torque\"],\n",
    "        shape=(1,),\n",
    "        dtype=torch.float32,\n",
    "    )\n",
    "    self.reward_spec = UnboundedContinuousTensorSpec(shape=(*td_params.shape, 1))\n",
    "\n",
    "\n",
    "def make_composite_from_td(td):\n",
    "    # custom function to convert a ``tensordict`` in a similar spec structure\n",
    "    # of unbounded values.\n",
    "    composite = CompositeSpec(\n",
    "        {\n",
    "            key: make_composite_from_td(tensor)\n",
    "            if isinstance(tensor, TensorDictBase)\n",
    "            else UnboundedContinuousTensorSpec(\n",
    "                dtype=tensor.dtype, device=tensor.device, shape=tensor.shape\n",
    "            )\n",
    "            for key, tensor in td.items()\n",
    "        },\n",
    "        shape=td.shape,\n",
    "    )\n",
    "    return composite"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Reproducible experiments: seeding\n",
    "=================================\n",
    "\n",
    "Seeding an environment is a common operation when initializing an\n",
    "experiment. The only goal of `EnvBase._set_seed`{.interpreted-text\n",
    "role=\"func\"} is to set the seed of the contained simulator. If possible,\n",
    "this operation should not call `reset()` or interact with the\n",
    "environment execution. The parent `EnvBase.set_seed`{.interpreted-text\n",
    "role=\"func\"} method incorporates a mechanism that allows seeding\n",
    "multiple environments with a different pseudo-random and reproducible\n",
    "seed.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def _set_seed(self, seed: Optional[int]):\n",
    "    rng = torch.manual_seed(seed)\n",
    "    self.rng = rng"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Wrapping things together: the `~torchrl.envs.EnvBase`{.interpreted-text role=\"class\"} class\n",
    "===========================================================================================\n",
    "\n",
    "We can finally put together the pieces and design our environment class.\n",
    "The specs initialization needs to be performed during the environment\n",
    "construction, so we must take care of calling the\n",
    "`_make_spec`{.interpreted-text role=\"func\"} method within\n",
    "`PendulumEnv.__init__`{.interpreted-text role=\"func\"}.\n",
    "\n",
    "We add a static method `PendulumEnv.gen_params`{.interpreted-text\n",
    "role=\"meth\"} which deterministically generates a set of hyperparameters\n",
    "to be used during execution:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def gen_params(g=10.0, batch_size=None) -> TensorDictBase:\n",
    "    \"\"\"Returns a ``tensordict`` containing the physical parameters such as gravitational force and torque or speed limits.\"\"\"\n",
    "    if batch_size is None:\n",
    "        batch_size = []\n",
    "    td = TensorDict(\n",
    "        {\n",
    "            \"params\": TensorDict(\n",
    "                {\n",
    "                    \"max_speed\": 8,\n",
    "                    \"max_torque\": 2.0,\n",
    "                    \"dt\": 0.05,\n",
    "                    \"g\": g,\n",
    "                    \"m\": 1.0,\n",
    "                    \"l\": 1.0,\n",
    "                },\n",
    "                [],\n",
    "            )\n",
    "        },\n",
    "        [],\n",
    "    )\n",
    "    if batch_size:\n",
    "        td = td.expand(batch_size).contiguous()\n",
    "    return td"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We define the environment as non-`batch_locked` by turning the\n",
    "`homonymous` attribute to `False`. This means that we will **not**\n",
    "enforce the input `tensordict` to have a `batch-size` that matches the\n",
    "one of the environment.\n",
    "\n",
    "The following code will just put together the pieces we have coded\n",
    "above.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class PendulumEnv(EnvBase):\n",
    "    metadata = {\n",
    "        \"render_modes\": [\"human\", \"rgb_array\"],\n",
    "        \"render_fps\": 30,\n",
    "    }\n",
    "    batch_locked = False\n",
    "\n",
    "    def __init__(self, td_params=None, seed=None, device=\"cpu\"):\n",
    "        if td_params is None:\n",
    "            td_params = self.gen_params()\n",
    "\n",
    "        super().__init__(device=device, batch_size=[])\n",
    "        self._make_spec(td_params)\n",
    "        if seed is None:\n",
    "            seed = torch.empty((), dtype=torch.int64).random_().item()\n",
    "        self.set_seed(seed)\n",
    "\n",
    "    # Helpers: _make_step and gen_params\n",
    "    gen_params = staticmethod(gen_params)\n",
    "    _make_spec = _make_spec\n",
    "\n",
    "    # Mandatory methods: _step, _reset and _set_seed\n",
    "    _reset = _reset\n",
    "    _step = staticmethod(_step)\n",
    "    _set_seed = _set_seed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Testing our environment\n",
    "=======================\n",
    "\n",
    "TorchRL provides a simple function\n",
    "`~torchrl.envs.utils.check_env_specs`{.interpreted-text role=\"func\"} to\n",
    "check that a (transformed) environment has an input/output structure\n",
    "that matches the one dictated by its specs. Let us try it out:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2024-11-17 23:03:13,172 [torchrl][INFO] check_env_specs succeeded!\n"
     ]
    }
   ],
   "source": [
    "env = PendulumEnv()\n",
    "check_env_specs(env)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can have a look at our specs to have a visual representation of the\n",
    "environment signature:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "observation_spec: CompositeSpec(\n",
      "    th: BoundedTensorSpec(\n",
      "        shape=torch.Size([]),\n",
      "        space=ContinuousBox(\n",
      "            low=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True),\n",
      "            high=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True)),\n",
      "        device=cpu,\n",
      "        dtype=torch.float32,\n",
      "        domain=continuous),\n",
      "    thdot: BoundedTensorSpec(\n",
      "        shape=torch.Size([]),\n",
      "        space=ContinuousBox(\n",
      "            low=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True),\n",
      "            high=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True)),\n",
      "        device=cpu,\n",
      "        dtype=torch.float32,\n",
      "        domain=continuous),\n",
      "    params: CompositeSpec(\n",
      "        max_speed: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.int64,\n",
      "            domain=discrete),\n",
      "        max_torque: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        dt: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        g: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        m: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        l: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous), device=cpu, shape=torch.Size([])), device=cpu, shape=torch.Size([]))\n",
      "state_spec: CompositeSpec(\n",
      "    th: BoundedTensorSpec(\n",
      "        shape=torch.Size([]),\n",
      "        space=ContinuousBox(\n",
      "            low=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True),\n",
      "            high=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True)),\n",
      "        device=cpu,\n",
      "        dtype=torch.float32,\n",
      "        domain=continuous),\n",
      "    thdot: BoundedTensorSpec(\n",
      "        shape=torch.Size([]),\n",
      "        space=ContinuousBox(\n",
      "            low=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True),\n",
      "            high=Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, contiguous=True)),\n",
      "        device=cpu,\n",
      "        dtype=torch.float32,\n",
      "        domain=continuous),\n",
      "    params: CompositeSpec(\n",
      "        max_speed: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.int64,\n",
      "            domain=discrete),\n",
      "        max_torque: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        dt: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        g: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        m: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous),\n",
      "        l: UnboundedContinuousTensorSpec(\n",
      "            shape=torch.Size([]),\n",
      "            space=None,\n",
      "            device=cpu,\n",
      "            dtype=torch.float32,\n",
      "            domain=continuous), device=cpu, shape=torch.Size([])), device=cpu, shape=torch.Size([]))\n",
      "reward_spec: UnboundedContinuousTensorSpec(\n",
      "    shape=torch.Size([1]),\n",
      "    space=ContinuousBox(\n",
      "        low=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, contiguous=True),\n",
      "        high=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, contiguous=True)),\n",
      "    device=cpu,\n",
      "    dtype=torch.float32,\n",
      "    domain=continuous)\n"
     ]
    }
   ],
   "source": [
    "print(\"observation_spec:\", env.observation_spec)\n",
    "print(\"state_spec:\", env.state_spec)\n",
    "print(\"reward_spec:\", env.reward_spec)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can execute a couple of commands too to check that the output\n",
    "structure matches what is expected.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "reset tensordict TensorDict(\n",
      "    fields={\n",
      "        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([]),\n",
      "    device=None,\n",
      "    is_shared=False)\n"
     ]
    }
   ],
   "source": [
    "td = env.reset()\n",
    "print(\"reset tensordict\", td)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can run the `env.rand_step`{.interpreted-text role=\"func\"} to\n",
    "generate an action randomly from the `action_spec` domain. A\n",
    "`tensordict` containing the hyperparameters and the current state\n",
    "**must** be passed since our environment is stateless. In stateful\n",
    "contexts, `env.rand_step()` works perfectly too.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "random step tensordict TensorDict(\n",
      "    fields={\n",
      "        action: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        next: TensorDict(\n",
      "            fields={\n",
      "                done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                params: TensorDict(\n",
      "                    fields={\n",
      "                        dt: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        g: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        l: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        m: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        max_speed: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                        max_torque: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "                    batch_size=torch.Size([]),\n",
      "                    device=None,\n",
      "                    is_shared=False),\n",
      "                reward: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                th: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                thdot: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([]),\n",
      "    device=None,\n",
      "    is_shared=False)\n"
     ]
    }
   ],
   "source": [
    "td = env.rand_step(td)\n",
    "print(\"random step tensordict\", td)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Transforming an environment\n",
    "===========================\n",
    "\n",
    "Writing environment transforms for stateless simulators is slightly more\n",
    "complicated than for stateful ones: transforming an output entry that\n",
    "needs to be read at the following iteration requires to apply the\n",
    "inverse transform before calling `meth.step`{.interpreted-text\n",
    "role=\"func\"} at the next step. This is an ideal scenario to showcase all\n",
    "the features of TorchRL\\'s transforms!\n",
    "\n",
    "For instance, in the following transformed environment we `unsqueeze`\n",
    "the entries `[\"th\", \"thdot\"]` to be able to stack them along the last\n",
    "dimension. We also pass them as `in_keys_inv` to squeeze them back to\n",
    "their original shape once they are passed as input in the next\n",
    "iteration.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "env = TransformedEnv(\n",
    "    env,\n",
    "    # ``Unsqueeze`` the observations that we will concatenate\n",
    "    UnsqueezeTransform(\n",
    "        unsqueeze_dim=-1,\n",
    "        in_keys=[\"th\", \"thdot\"],\n",
    "        in_keys_inv=[\"th\", \"thdot\"],\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Writing custom transforms\n",
    "=========================\n",
    "\n",
    "TorchRL\\'s transforms may not cover all the operations one wants to\n",
    "execute after an environment has been executed. Writing a transform does\n",
    "not require much effort. As for the environment design, there are two\n",
    "steps in writing a transform:\n",
    "\n",
    "-   Getting the dynamics right (forward and inverse);\n",
    "-   Adapting the environment specs.\n",
    "\n",
    "A transform can be used in two settings: on its own, it can be used as a\n",
    "`~torch.nn.Module`{.interpreted-text role=\"class\"}. It can also be used\n",
    "appended to a\n",
    "`~torchrl.envs.transforms.TransformedEnv`{.interpreted-text\n",
    "role=\"class\"}. The structure of the class allows to customize the\n",
    "behavior in the different contexts.\n",
    "\n",
    "A `~torchrl.envs.transforms.Transform`{.interpreted-text role=\"class\"}\n",
    "skeleton can be summarized as follows:\n",
    "\n",
    "``` {.}\n",
    "class Transform(nn.Module):\n",
    "    def forward(self, tensordict):\n",
    "        ...\n",
    "    def _apply_transform(self, tensordict):\n",
    "        ...\n",
    "    def _step(self, tensordict):\n",
    "        ...\n",
    "    def _call(self, tensordict):\n",
    "        ...\n",
    "    def inv(self, tensordict):\n",
    "        ...\n",
    "    def _inv_apply_transform(self, tensordict):\n",
    "        ...\n",
    "```\n",
    "\n",
    "There are three entry points (`forward`{.interpreted-text role=\"func\"},\n",
    "`_step`{.interpreted-text role=\"func\"} and `inv`{.interpreted-text\n",
    "role=\"func\"}) which all receive\n",
    "`tensordict.TensorDict`{.interpreted-text role=\"class\"} instances. The\n",
    "first two will eventually go through the keys indicated by\n",
    "`~tochrl.envs.transforms.Transform.in_keys`{.interpreted-text\n",
    "role=\"obj\"} and call\n",
    "`~torchrl.envs.transforms.Transform._apply_transform`{.interpreted-text\n",
    "role=\"meth\"} to each of these. The results will be written in the\n",
    "entries pointed by `Transform.out_keys`{.interpreted-text role=\"obj\"} if\n",
    "provided (if not the `in_keys` will be updated with the transformed\n",
    "values). If inverse transforms need to be executed, a similar data flow\n",
    "will be executed but with the `Transform.inv`{.interpreted-text\n",
    "role=\"func\"} and `Transform._inv_apply_transform`{.interpreted-text\n",
    "role=\"func\"} methods and across the `in_keys_inv` and `out_keys_inv`\n",
    "list of keys. The following figure summarized this flow for environments\n",
    "and replay buffers.\n",
    "\n",
    "> Transform API\n",
    "\n",
    "In some cases, a transform will not work on a subset of keys in a\n",
    "unitary manner, but will execute some operation on the parent\n",
    "environment or work with the entire input `tensordict`. In those cases,\n",
    "the `_call`{.interpreted-text role=\"func\"} and\n",
    "`forward`{.interpreted-text role=\"func\"} methods should be re-written,\n",
    "and the `_apply_transform`{.interpreted-text role=\"func\"} method can be\n",
    "skipped.\n",
    "\n",
    "Let us code new transforms that will compute the `sine` and `cosine`\n",
    "values of the position angle, as these values are more useful to us to\n",
    "learn a policy than the raw angle value:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class SinTransform(Transform):\n",
    "    def _apply_transform(self, obs: torch.Tensor) -> None:\n",
    "        return obs.sin()\n",
    "\n",
    "    # The transform must also modify the data at reset time\n",
    "    def _reset(\n",
    "        self, tensordict: TensorDictBase, tensordict_reset: TensorDictBase\n",
    "    ) -> TensorDictBase:\n",
    "        return self._call(tensordict_reset)\n",
    "\n",
    "    # _apply_to_composite will execute the observation spec transform across all\n",
    "    # in_keys/out_keys pairs and write the result in the observation_spec which\n",
    "    # is of type ``Composite``\n",
    "    @_apply_to_composite\n",
    "    def transform_observation_spec(self, observation_spec):\n",
    "        return BoundedTensorSpec(\n",
    "            low=-1,\n",
    "            high=1,\n",
    "            shape=observation_spec.shape,\n",
    "            dtype=observation_spec.dtype,\n",
    "            device=observation_spec.device,\n",
    "        )\n",
    "\n",
    "\n",
    "class CosTransform(Transform):\n",
    "    def _apply_transform(self, obs: torch.Tensor) -> None:\n",
    "        return obs.cos()\n",
    "\n",
    "    # The transform must also modify the data at reset time\n",
    "    def _reset(\n",
    "        self, tensordict: TensorDictBase, tensordict_reset: TensorDictBase\n",
    "    ) -> TensorDictBase:\n",
    "        return self._call(tensordict_reset)\n",
    "\n",
    "    # _apply_to_composite will execute the observation spec transform across all\n",
    "    # in_keys/out_keys pairs and write the result in the observation_spec which\n",
    "    # is of type ``Composite``\n",
    "    @_apply_to_composite\n",
    "    def transform_observation_spec(self, observation_spec):\n",
    "        return BoundedTensorSpec(\n",
    "            low=-1,\n",
    "            high=1,\n",
    "            shape=observation_spec.shape,\n",
    "            dtype=observation_spec.dtype,\n",
    "            device=observation_spec.device,\n",
    "        )\n",
    "\n",
    "\n",
    "t_sin = SinTransform(in_keys=[\"th\"], out_keys=[\"sin\"])\n",
    "t_cos = CosTransform(in_keys=[\"th\"], out_keys=[\"cos\"])\n",
    "env.append_transform(t_sin)\n",
    "env.append_transform(t_cos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Concatenates the observations onto an \\\"observation\\\" entry.\n",
    "`del_keys=False` ensures that we keep these values for the next\n",
    "iteration.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "cat_transform = CatTensors(\n",
    "    in_keys=[\"sin\", \"cos\", \"thdot\"], dim=-1, out_key=\"observation\", del_keys=False\n",
    ")\n",
    "env.append_transform(cat_transform)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once more, let us check that our environment specs match what is\n",
    "received:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2024-11-17 23:03:13,247 [torchrl][INFO] check_env_specs succeeded!\n"
     ]
    }
   ],
   "source": [
    "check_env_specs(env)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Executing a rollout\n",
    "===================\n",
    "\n",
    "Executing a rollout is a succession of simple steps:\n",
    "\n",
    "-   reset the environment\n",
    "-   while some condition is not met:\n",
    "    -   compute an action given a policy\n",
    "    -   execute a step given this action\n",
    "    -   collect the data\n",
    "    -   make a `MDP` step\n",
    "-   gather the data and return\n",
    "\n",
    "These operations have been conveniently wrapped in the\n",
    "`~torchrl.envs.EnvBase.rollout`{.interpreted-text role=\"meth\"} method,\n",
    "from which we provide a simplified version here below.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "data from rollout: TensorDict(\n",
      "    fields={\n",
      "        action: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        cos: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        done: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        next: TensorDict(\n",
      "            fields={\n",
      "                cos: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                done: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                observation: Tensor(shape=torch.Size([100, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                params: TensorDict(\n",
      "                    fields={\n",
      "                        dt: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        g: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        l: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        m: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        max_speed: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                        max_torque: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "                    batch_size=torch.Size([100]),\n",
      "                    device=None,\n",
      "                    is_shared=False),\n",
      "                reward: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                sin: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                terminated: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                th: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                thdot: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([100]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        observation: Tensor(shape=torch.Size([100, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([100]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([100]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        sin: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([100, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([100]),\n",
      "    device=None,\n",
      "    is_shared=False)\n"
     ]
    }
   ],
   "source": [
    "def simple_rollout(steps=100):\n",
    "    # preallocate:\n",
    "    data = TensorDict({}, [steps])\n",
    "    # reset\n",
    "    _data = env.reset()\n",
    "    for i in range(steps):\n",
    "        _data[\"action\"] = env.action_spec.rand()\n",
    "        _data = env.step(_data)\n",
    "        data[i] = _data\n",
    "        _data = step_mdp(_data, keep_other=True)\n",
    "    return data\n",
    "\n",
    "\n",
    "print(\"data from rollout:\", simple_rollout(100))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Batching computations\n",
    "=====================\n",
    "\n",
    "The last unexplored end of our tutorial is the ability that we have to\n",
    "batch computations in TorchRL. Because our environment does not make any\n",
    "assumptions regarding the input data shape, we can seamlessly execute it\n",
    "over batches of data. Even better: for non-batch-locked environments\n",
    "such as our Pendulum, we can change the batch size on the fly without\n",
    "recreating the environment. To do this, we just generate parameters with\n",
    "the desired shape.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "reset (batch size of 10) TensorDict(\n",
      "    fields={\n",
      "        cos: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        done: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        observation: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([10]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        sin: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([10]),\n",
      "    device=None,\n",
      "    is_shared=False)\n",
      "rand step (batch size of 10) TensorDict(\n",
      "    fields={\n",
      "        action: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        cos: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        done: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        next: TensorDict(\n",
      "            fields={\n",
      "                cos: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                done: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                observation: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                params: TensorDict(\n",
      "                    fields={\n",
      "                        dt: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        g: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        l: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        m: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        max_speed: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                        max_torque: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "                    batch_size=torch.Size([10]),\n",
      "                    device=None,\n",
      "                    is_shared=False),\n",
      "                reward: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                sin: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                terminated: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                th: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                thdot: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([10]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        observation: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([10]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([10]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        sin: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([10, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([10]),\n",
      "    device=None,\n",
      "    is_shared=False)\n"
     ]
    }
   ],
   "source": [
    "batch_size = 10  # number of environments to be executed in batch\n",
    "td = env.reset(env.gen_params(batch_size=[batch_size]))\n",
    "print(\"reset (batch size of 10)\", td)\n",
    "td = env.rand_step(td)\n",
    "print(\"rand step (batch size of 10)\", td)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Executing a rollout with a batch of data requires us to reset the\n",
    "environment out of the rollout function, since we need to define the\n",
    "batch\\_size dynamically and this is not supported by\n",
    "`~torchrl.envs.EnvBase.rollout`{.interpreted-text role=\"meth\"}:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "rollout of len 3 (batch size of 10): TensorDict(\n",
      "    fields={\n",
      "        action: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        cos: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        done: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        next: TensorDict(\n",
      "            fields={\n",
      "                cos: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                done: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                observation: Tensor(shape=torch.Size([10, 3, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                params: TensorDict(\n",
      "                    fields={\n",
      "                        dt: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        g: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        l: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        m: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                        max_speed: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                        max_torque: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "                    batch_size=torch.Size([10, 3]),\n",
      "                    device=None,\n",
      "                    is_shared=False),\n",
      "                reward: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                sin: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                terminated: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "                th: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                thdot: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([10, 3]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        observation: Tensor(shape=torch.Size([10, 3, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        params: TensorDict(\n",
      "            fields={\n",
      "                dt: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                g: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                l: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                m: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "                max_speed: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.int64, is_shared=False),\n",
      "                max_torque: Tensor(shape=torch.Size([10, 3]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "            batch_size=torch.Size([10, 3]),\n",
      "            device=None,\n",
      "            is_shared=False),\n",
      "        sin: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        terminated: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.bool, is_shared=False),\n",
      "        th: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False),\n",
      "        thdot: Tensor(shape=torch.Size([10, 3, 1]), device=cpu, dtype=torch.float32, is_shared=False)},\n",
      "    batch_size=torch.Size([10, 3]),\n",
      "    device=None,\n",
      "    is_shared=False)\n"
     ]
    }
   ],
   "source": [
    "rollout = env.rollout(\n",
    "    3,\n",
    "    auto_reset=False,  # we're executing the reset out of the ``rollout`` call\n",
    "    tensordict=env.reset(env.gen_params(batch_size=[batch_size])),\n",
    ")\n",
    "print(\"rollout of len 3 (batch size of 10):\", rollout)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Training a simple policy\n",
    "========================\n",
    "\n",
    "In this example, we will train a simple policy using the reward as a\n",
    "differentiable objective, such as a negative loss. We will take\n",
    "advantage of the fact that our dynamic system is fully differentiable to\n",
    "backpropagate through the trajectory return and adjust the weights of\n",
    "our policy to maximize this value directly. Of course, in many settings\n",
    "many of the assumptions we make do not hold, such as differentiable\n",
    "system and full access to the underlying mechanics.\n",
    "\n",
    "Still, this is a very simple example that showcases how a training loop\n",
    "can be coded with a custom environment in TorchRL.\n",
    "\n",
    "Let us first write the policy network:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "torch.manual_seed(0)\n",
    "env.set_seed(0)\n",
    "\n",
    "net = nn.Sequential(\n",
    "    nn.LazyLinear(64),\n",
    "    nn.Tanh(),\n",
    "    nn.LazyLinear(64),\n",
    "    nn.Tanh(),\n",
    "    nn.LazyLinear(64),\n",
    "    nn.Tanh(),\n",
    "    nn.LazyLinear(1),\n",
    ")\n",
    "policy = TensorDictModule(\n",
    "    net,\n",
    "    in_keys=[\"observation\"],\n",
    "    out_keys=[\"action\"],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and our optimizer:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "optim = torch.optim.Adam(policy.parameters(), lr=2e-3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Training loop\n",
    "=============\n",
    "\n",
    "We will successively:\n",
    "\n",
    "-   generate a trajectory\n",
    "-   sum the rewards\n",
    "-   backpropagate through the graph defined by these operations\n",
    "-   clip the gradient norm and make an optimization step\n",
    "-   repeat\n",
    "\n",
    "At the end of the training loop, we should have a final reward close to\n",
    "0 which demonstrates that the pendulum is upward and still as desired.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzkAAAHWCAYAAABUn0dnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADX1UlEQVR4nOydd5gb1fX3vzOSVtvXa3vXvVdsXHDFgLFN7xB6SAATQiAhJAETYhNqQjAEE34vJAQICRBK6CUhoduA6cUYsMG94l63r1Zl3j+kGd25c6dJo7rn8zx+LE29mpXuveeec75HUhRFAUEQBEEQBEEQRJEg57oBBEEQBEEQBEEQXkJGDkEQBEEQBEEQRQUZOQRBEARBEARBFBVk5BAEQRAEQRAEUVSQkUMQBEEQBEEQRFFBRg5BEARBEARBEEUFGTkEQRAEQRAEQRQVZOQQBEEQBEEQBFFUkJFDEARBEARBEERRQUYOQRAEQRBEijz88MOQJAkbNmzIdVOKkg0bNkCSJDz88MO5bgpRYJCRQxAueOKJJ/B///d/uW4GQRAE0Um49dZb8eKLL+a6GQRRcJCRQxAuICOHIAiCyCZk5BBEapCRQ3R6Wlpact0EtLa25roJBEEQRBHS3t6OWCyW62ZYQmMgkQnIyCE6FTfddBMkScI333yD8847D7W1tTjssMMAAI899hgmTpyIsrIydO3aFeeeey42b96snTtz5kz897//xcaNGyFJEiRJwsCBAwGYx2S//fbbkCQJb7/9tu46Bx54ID7//HMcfvjhKC8vx7XXXqvFHS9YsAAPPPAAhgwZgmAwiMmTJ+PTTz/VXXf79u246KKL0LdvXwSDQfTq1QunnnoqxYQTBEHkAS+99BJOPPFE9O7dG8FgEEOGDMHvf/97RKNR3XGrV6/GGWecgZ49e6K0tBR9+/bFueeei4aGBgCAJEloaWnBI488oo07s2fPNr2vOuY8+eSTuO6669CnTx+Ul5ejsbERAPDxxx/juOOOQ01NDcrLyzFjxgy8//772vlfffUVJEnCv//9b23b559/DkmSMGHCBN29jj/+eEydOtX1ZzYbAwFg//79mD17NmpqatClSxdceOGF2L9/v/MHTxAM/lw3gCBywVlnnYVhw4bh1ltvhaIo+MMf/oDrr78eZ599Nn784x9j165duOeee3D44Yfjiy++QJcuXfDb3/4WDQ0N+O6773DXXXcBACorK1O6/549e3D88cfj3HPPxQ9/+EP06NFD2/fEE0+gqakJl156KSRJwh//+EecfvrpWLduHQKBAADgjDPOwPLly3HFFVdg4MCB2LlzJ9544w1s2rRJM7wIgiCI3PDwww+jsrISV111FSorK7Fw4ULccMMNaGxsxB133AEA6OjowLHHHotQKIQrrrgCPXv2xJYtW/Dyyy9j//79qKmpwaOPPoof//jHmDJlCn7yk58AAIYMGWJ7/9///vcoKSnB1VdfjVAohJKSEixcuBDHH388Jk6ciBtvvBGyLOOhhx7CEUccgcWLF2PKlCk48MAD0aVLF7z77rs45ZRTAACLFy+GLMv48ssv0djYiOrqasRiMXzwwQdam5x+ZhXRGKgoCk499VS89957uOyyy3DAAQfghRdewIUXXujVn4XobCgE0Ym48cYbFQDK97//fW3bhg0bFJ/Pp/zhD3/QHfv1118rfr9ft/3EE09UBgwYYLjuQw89pABQ1q9fr9u+aNEiBYCyaNEibduMGTMUAMp9992nO3b9+vUKAKVbt27K3r17te0vvfSSAkD5z3/+oyiKouzbt08BoNxxxx1uPz5BEAThMaL+v7W11XDcpZdeqpSXlyvt7e2KoijKF198oQBQnnnmGcvrV1RUKBdeeKGjtqhjzuDBg3VtiMViyrBhw5Rjjz1WicViunYOGjRIOfroo7VtJ554ojJlyhTt/emnn66cfvrpis/nU1555RVFURRlyZIlCgDlpZdecvWZFcV8DHzxxRcVAMof//hHbVskElGmT5+uAFAeeughR8+AIFQoXI3olFx22WXa6+effx6xWAxnn302du/erf3r2bMnhg0bhkWLFnl+/2AwiIsuuki475xzzkFtba32fvr06QCAdevWAQDKyspQUlKCt99+G/v27fO8bQRBEER6lJWVaa+bmpqwe/duTJ8+Ha2trVixYgUAoKamBgDw2muveZ6TcuGFF+rasHTpUqxevRrnnXce9uzZo41zLS0tOPLII/Huu+9qeTvTp0/HkiVLtHzV9957DyeccALGjx+PxYsXA4h7dyRJ0sK9nX5mFdEY+L///Q9+vx8//elPtW0+nw9XXHGFR0+F6GxQuBrRKRk0aJD2evXq1VAUBcOGDRMeq4aIeUmfPn1QUlIi3Ne/f3/de9Xg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pvHjx2vRokXKyspSVFSU8vPz9fvf/75Z2hkWFub32uVyqaamplnOBQAILHIT0HgUOWi1PB6Pqqur/bbdc889OnTokLp3796oz9q6datSUlL01FNPWdtOnDhxy/PV1atXL+Xl5enixYtWsvr444/ldrt11113NapNAAD7ITcBTYOFB9Bqpaam6sMPP9SpU6d09uxZSVdXodm6daumTZumwsJCHT16VKtXr643ubKuHj16qLi4WPn5+Tp+/LiWLFmilStX1jtfUVGRCgsLdfbsWVVWVtb7nPHjx6tNmzbKzs7WgQMHVFBQoMcff1yPPvqoNRwAAOBc5CagaVDkoNVavHixvvjiC33ve99TbGysJKl///7avHmzjhw5oqFDh2rAgAGaP3++kpKSbvpZo0aN0hNPPKFp06bp7rvv1tatW62VbWqNGTNGI0aM0AMPPKDY2Fi9/vrr9T4nIiJC69at07lz53Tvvffq4Ycf1vDhw/XHP/6x6QIHAAQtchPQNFzGGBPoRgAAAABAU6EnBwAAAICjUOQAAAAAcBSKHAAAAACOQpEDAAAAwFEocgAAAAA4CkUOAAAAAEehyAEAAADgKBQ5AAAAAByFIgcAAACAo1DkAAAAAHAUihwAAAAAjvJ/a2ktapvv0o4AAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "batch_size = 32\n",
    "pbar = tqdm.tqdm(range(20_000 // batch_size))\n",
    "scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optim, 20_000)\n",
    "logs = defaultdict(list)\n",
    "\n",
    "for _ in pbar:\n",
    "    init_td = env.reset(env.gen_params(batch_size=[batch_size]))\n",
    "    rollout = env.rollout(100, policy, tensordict=init_td, auto_reset=False)\n",
    "    traj_return = rollout[\"next\", \"reward\"].mean()\n",
    "    (-traj_return).backward()\n",
    "    gn = torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0)\n",
    "    optim.step()\n",
    "    optim.zero_grad()\n",
    "    pbar.set_description(\n",
    "        f\"reward: {traj_return: 4.4f}, \"\n",
    "        f\"last reward: {rollout[..., -1]['next', 'reward'].mean(): 4.4f}, gradient norm: {gn: 4.4}\"\n",
    "    )\n",
    "    logs[\"return\"].append(traj_return.item())\n",
    "    logs[\"last_reward\"].append(rollout[..., -1][\"next\", \"reward\"].mean().item())\n",
    "    scheduler.step()\n",
    "\n",
    "\n",
    "def plot():\n",
    "    import matplotlib\n",
    "    from matplotlib import pyplot as plt\n",
    "\n",
    "    is_ipython = \"inline\" in matplotlib.get_backend()\n",
    "    if is_ipython:\n",
    "        from IPython import display\n",
    "\n",
    "    with plt.ion():\n",
    "        plt.figure(figsize=(10, 5))\n",
    "        plt.subplot(1, 2, 1)\n",
    "        plt.plot(logs[\"return\"])\n",
    "        plt.title(\"returns\")\n",
    "        plt.xlabel(\"iteration\")\n",
    "        plt.subplot(1, 2, 2)\n",
    "        plt.plot(logs[\"last_reward\"])\n",
    "        plt.title(\"last reward\")\n",
    "        plt.xlabel(\"iteration\")\n",
    "        if is_ipython:\n",
    "            display.display(plt.gcf())\n",
    "            display.clear_output(wait=True)\n",
    "        plt.show()\n",
    "\n",
    "\n",
    "plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Conclusion\n",
    "==========\n",
    "\n",
    "In this tutorial, we have learned how to code a stateless environment\n",
    "from scratch. We touched the subjects of:\n",
    "\n",
    "-   The four essential components that need to be taken care of when\n",
    "    coding an environment (`step`, `reset`, seeding and building specs).\n",
    "    We saw how these methods and classes interact with the\n",
    "    `~tensordict.TensorDict`{.interpreted-text role=\"class\"} class;\n",
    "-   How to test that an environment is properly coded using\n",
    "    `~torchrl.envs.utils.check_env_specs`{.interpreted-text\n",
    "    role=\"func\"};\n",
    "-   How to append transforms in the context of stateless environments\n",
    "    and how to write custom transformations;\n",
    "-   How to train a policy on a fully differentiable simulator.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "mac-rl",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
